package com.biwin.module.jwt.starter.session;

import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.StrUtil;
import com.biwin.common.api.constant.BwSystemConstant;
import com.biwin.core.api.session.ISessionProvider;
import com.biwin.core.main.event.SystemUserPasswordChangeEvent;
import com.biwin.module.jwt.api.exception.TokenAuthenticationException;
import com.biwin.module.jwt.api.properties.BiWinJwtProperties;
import com.biwin.module.security.api.vo.BwUserDetailVo;
import io.jsonwebtoken.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.ApplicationListener;
import org.springframework.data.redis.core.*;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.stereotype.Component;

import java.nio.charset.StandardCharsets;
import java.util.*;
import java.util.concurrent.TimeUnit;
import java.util.function.BinaryOperator;
import java.util.stream.Collectors;

/**
 * 主要完成操作 redis 中的 token 用户,而主要的 token 认证机制交由 SessionManager 去处理
 *
 * @author biwin
 * @since 0.0.1
 */
@Component
public class BwRedisSessionProvider<T extends BwUserDetailVo> implements ISessionProvider<T>,
        ApplicationListener<SystemUserPasswordChangeEvent> {
    private static final Logger log = LoggerFactory.getLogger(BwRedisSessionProvider.class);
    private static final String AUTHORITIES_KEY = "auth";
    private static final String JWT_KEY = "JWT";
    private static final String USER_KEY = "USER";

    private final RedisTemplate<String, BwUserDetailVo> redisTemplate;
    private final StringRedisTemplate stringRedisTemplate;
    private final BiWinJwtProperties jwtProperties;
    private static final ThreadLocal<Set<String>> threadLocal = new ThreadLocal<>();

    public BwRedisSessionProvider(RedisTemplate bwRedisTemplate,
                                  StringRedisTemplate stringRedisTemplate, BiWinJwtProperties jwtProperties) {
        this.redisTemplate = bwRedisTemplate;
        this.stringRedisTemplate = stringRedisTemplate;
        this.jwtProperties = jwtProperties;
    }

    @Override
    public T getUserInfo(String account) {
        return (T) redisTemplate.opsForValue().get(account);
    }

    @Override
    public void setUserInfo(T onlineUserInfoVo) {
        String username = onlineUserInfoVo.getUsername();
        Set<String> keys = scan(buildOnlineUserJwtRedisKey(username));
        if (CollUtil.isEmpty(keys)) {
            redisTemplate.opsForValue().set(buildOnlineUserInfoRedisKey(username), onlineUserInfoVo,
                    jwtProperties.getTtl(), jwtProperties.getTimeUnit());
        }
        // 关于 user 在 redis 的续命问题交由 tokenRenew 去解决
    }


    @Override
    public String createJwtToken(T authentication, long timeOut) {
        // developer
        String username = authentication.getUsername();
        //  BIWIN:BIWIN:developer
        String redisKey = buildOnlineUserJwtRedisKey(username);
        int index = handlerRedisKeyIndex(redisKey, Integer::max) + 1;
        // BIWIN:BIWIN:developer:0
        String writeRedisKey = redisKey + BwSystemConstant.COLON + index;

        // 获取权限
        String authorities = authentication.getAuthorities().stream()
                .map(GrantedAuthority::getAuthority)
                .collect(Collectors.joining(","));

        // 生成 token
        String token = buildToken(username + BwSystemConstant.COLON + index, authorities);

        setUserInfo(authentication);

        stringRedisTemplate.opsForValue().set(writeRedisKey, token, jwtProperties.getTtl(),
                jwtProperties.getTimeUnit());

        if (log.isDebugEnabled()) {
            log.debug("当前用户: {} 生成的token: {}", username, token);
        }

        // 登录所有动作完成清除掉 threadLocal ,防止内存泄露
        threadLocal.remove();
        return token;
    }

    private String buildToken(String username, String authorities) {
        // 过期由 redis 来完成, 暂未使用 jwt 自带的过期
        //        Date validity = new Date(System.currentTimeMillis() + timeOut);
        String token = Jwts.builder()
                .setSubject(username)
                .claim(AUTHORITIES_KEY, authorities)
                .signWith(SignatureAlgorithm.HS512, jwtProperties.getKey())
                //                .setExpiration(validity)
                .compact();
        return token;
    }


    @Override
    public String getJwtToken(String username) {
        return stringRedisTemplate.opsForValue().get(buildOnlineUserJwtRedisKey(username));
    }

    @Override
    public void invalidUserInfo(String account) {

    }

    @Override
    public boolean validateTokenStr(String authToken) {
        try {
            Jwts.parser().setSigningKey(jwtProperties.getKey()).parseClaimsJws(authToken);
            return true;
        } catch (SignatureException | MalformedJwtException e) {
            log.info("Invalid JWT signature.");
            e.printStackTrace();
        } catch (ExpiredJwtException e) {
            log.info("Expired JWT token.");
            e.printStackTrace();
        } catch (UnsupportedJwtException e) {
            log.info("Unsupported JWT token.");
            e.printStackTrace();
        } catch (IllegalArgumentException e) {
            log.info("JWT token compact of handler are invalid.");
            e.printStackTrace();
        }
        return false;
    }

    @Override
    public Authentication refreshToken(String token) {
        Claims claims = parseJwt(token);
        String username = claims.getSubject();
        String jwtRedisKey = buildOnlineUserJwtRedisKey(username);
        String redisToken = stringRedisTemplate.opsForValue().get(jwtRedisKey);

        if (StrUtil.equals(redisToken, token)) {
            Long expire = redisTemplate.getExpire(jwtRedisKey, TimeUnit.SECONDS);
            if (Objects.isNull(expire) || expire < 1) {
                throw new TokenAuthenticationException("认证信息已过期,请重新登录!");
            }
            if (expire < 600) {
                tokenRenew(jwtRedisKey);
            }
        } else {
            throw new TokenAuthenticationException("认证信息已过期,请重新登录!");
        }

        String auth = claims.get(AUTHORITIES_KEY).toString();
        Collection<? extends GrantedAuthority> authorities = new ArrayList<>();
        if (StrUtil.isNotBlank(auth)) {
            Arrays.stream(auth.split(","))
                    .map(SimpleGrantedAuthority::new)
                    .collect(Collectors.toList());
        }
        //        BwUserDetailVo principal = new BwUserDetailVo(claims.getSubject(), "", authorities);
        BwUserDetailVo principal = getUserFromRedis(username);
        return new UsernamePasswordAuthenticationToken(principal, token, authorities);
    }

    public BwUserDetailVo getUserFromRedis(String username) {
        String userRedisKey = buildOnlineUserInfoRedisKey(username);
        BwUserDetailVo bwUserDetailVo = redisTemplate.opsForValue().get(userRedisKey.substring(0,
                userRedisKey.lastIndexOf(BwSystemConstant.COLON)));
        bwUserDetailVo.setId(bwUserDetailVo.getSystemUserVo().getId());

        //        return JSONUtil.toBean((String) redisTemplate.opsForValue().get(userRedisKey.substring(0,
        //                userRedisKey.lastIndexOf(BwSystemConstant.COLON))), BwUserDetailVo.class);
        return bwUserDetailVo;
    }

    /**
     * 移除指定账号最先登录的 token
     *
     * @param username 登录账号
     */
    @Override
    public void logoutFirstToken(String username) {
        String redisKey = buildOnlineUserJwtRedisKey(username);

        Integer index = handlerRedisKeyIndex(redisKey, Integer::min);
        redisTemplate.delete(redisKey + BwSystemConstant.COLON + index);
    }

    /**
     * 根据传入的模糊在线用户的redisKey,进行最大或最小的index值返回
     *
     * @param jwtRedisKey BIWIN:BIWIN:developer
     * @param operator    Integer::max or Integer::min
     * @return 仅返回最大最小值.
     */
    private Integer handlerRedisKeyIndex(String jwtRedisKey, BinaryOperator<Integer> operator) {
        Set<String> keys = scan(jwtRedisKey);
        List<Integer> redisKeySet = keys.stream()
                .map(key -> Integer.parseInt(key.substring(key.lastIndexOf(BwSystemConstant.COLON) + 1)))
                .collect(Collectors.toList());
        Integer index = redisKeySet
                .stream().reduce(operator).orElse(0);
        return index;
    }

    /**
     * 检查指定用户的在线用户数是否大于
     *
     * @param username
     * @return true: 可以继续登录
     */
    @Override
    public boolean checkMaxNum(String username) {
        Set<String> online = scan(buildOnlineUserJwtRedisKey(username));
        return CollUtil.isEmpty(online) || (CollUtil.isNotEmpty(online) && online.size() < jwtProperties.getMaxNum());
    }

    /**
     * scan 实现
     *
     * @param pattern 表达式，如：abc*，找出所有以abc开始的键
     */
    private Set<String> scan(String pattern) {
        if (CollUtil.isNotEmpty(threadLocal.get())) {
            return threadLocal.get();
        }
        Set<String> keys = redisTemplate.execute((RedisCallback<Set<String>>) connection -> {
            Set<String> keysTmp = new HashSet<>();
            try (Cursor<byte[]> cursor = connection.scan(new ScanOptions.ScanOptionsBuilder()
                    .match(pattern + BwSystemConstant.COLON + BwSystemConstant.STAR)
                    .count(10000).build())) {

                while (cursor.hasNext()) {
                    keysTmp.add(new String(cursor.next(), StandardCharsets.UTF_8));
                }
            } catch (Exception e) {
                log.error(e.getMessage(), e);
                throw new RuntimeException(e);
            }
            return keysTmp;
        });
        threadLocal.set(keys);
        return keys;
    }

    private String buildOnlineUserJwtRedisKey(String username) {
        return buildOnlineUserJwtRedisKey(username, JWT_KEY);
    }

    private String buildOnlineUserJwtRedisKey(String username, String prefix) {
        StringBuilder stringBuilder = generalRedisKey();
        if (StrUtil.isNotBlank(prefix)) {
            stringBuilder.append(prefix)
                    .append(BwSystemConstant.COLON);
        }
        stringBuilder.append(username);
        if (log.isDebugEnabled()) {
            log.debug("存入 redis 的JWT key : {}", stringBuilder.toString());
        }
        return stringBuilder.toString();
    }

    private String buildOnlineUserInfoRedisKey(String username) {
        return buildOnlineUserInfoRedisKey(username, USER_KEY);
    }

    private String buildOnlineUserInfoRedisKey(String username, String prefix) {
        StringBuilder stringBuilder = generalRedisKey();
        if (StrUtil.isNotBlank(prefix)) {
            stringBuilder.append(prefix)
                    .append(BwSystemConstant.COLON);
        }
        if (log.isDebugEnabled()) {
            log.debug("存入 redis 的USER key: {}", stringBuilder.toString());
        }
        return stringBuilder.append(username).toString();
    }

    private StringBuilder generalRedisKey() {
        String project = jwtProperties.getProject();
        String prefix = jwtProperties.getPrefix();
        StringBuilder stringBuilder = new StringBuilder();
        if (StrUtil.isNotBlank(project)) {
            stringBuilder.append(prefix)
                    .append(BwSystemConstant.COLON);
        }
        if (StrUtil.isNotBlank(prefix)) {
            stringBuilder.append(prefix)
                    .append(BwSystemConstant.COLON);
        }
        return stringBuilder;
    }

    /**
     * 解析合法的 token 信息
     *
     * @param token
     * @return
     */
    private Claims parseJwt(String token) {
        return Jwts.parser().setSigningKey(jwtProperties.getKey()).parseClaimsJws(token).getBody();
    }

    /**
     * 将当前登录用户的 token 延长失效时间
     *
     * @param redisTokenKey 当前登录用户
     */
    private void tokenRenew(String redisTokenKey) {
        long ttl = jwtProperties.getTtl();
        TimeUnit timeUnit = jwtProperties.getTimeUnit();
        // 续命登录用户 redisKey 信息
        redisTemplate.expire(redisTokenKey, ttl, timeUnit);
        // 续命 User 信息
        redisTemplate.expire(redisTokenKey.substring(redisTokenKey.lastIndexOf(BwSystemConstant.COLON)), ttl, timeUnit);
    }

    @Override
    public void onApplicationEvent(SystemUserPasswordChangeEvent systemUserPasswordChangeEvent) {
        if (systemUserPasswordChangeEvent.getSource() instanceof String) {
            // todo 提出所有当前用户登录的在线用户
        }
    }
}
